﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace memcached
{
    /// <summary>
    /// Provide means to synchronize operations in a system-wide manner.
    /// Example usage would be to populate cache once with DB data and ensuring no other process is doing the same.
    /// Thread safe, cross-process synchronization.
    /// </summary>
    public static class DistMemCache
    {
        /// <summary>
        /// Perform the operation only in one instance per system and return result.
        /// The operation will not be performed by any other process in parallel.
        /// Thread safe.
        /// </summary>
        /// <remarks>
        /// Allows to implement a single-execution pattern, i.e. for resource heavy operation populating cache.
        /// Time slice is 0.25s but the minimum operation timeout is 1s.
        /// Do not use null as a valid value here.
        /// Be sure to set timeout to a value that reflects thread stalls.
        /// </remarks>
        /// <typeparam name="T">type of value</typeparam>
        /// <param name="client">store</param>
        /// <param name="maxOperationTimeout">limit in seconds for the operation</param>
        /// <param name="lockName">process-wide unique name</param>
        /// <param name="resultKey"></param>
        /// <param name="operation">operation that generates non-null result</param>
        /// <returns>proper value of T, or default(T) if Set failed or timed out waiting for other process</returns>
        public static T PerformOnce<T>(
            ICache client,
            int maxOperationTimeout,
            string lockName,
            string resultKey,
            Func<ICache, T> operation)
        {
            T output = default(T);
            Action op = () =>
            {
                DateTime start = DateTime.Now;
                object result = operation(client);// perform lengthy operation
                if (DateTime.Now.Subtract(start).TotalSeconds > maxOperationTimeout)
                {
                    System.Diagnostics.Trace.TraceWarning(
                        "PerformOnce: Operation for '{1}' lasted longer than maxOperationTimeout of {0}s",
                        maxOperationTimeout, resultKey);
                }
                if (result == null)
                {
                    System.Diagnostics.Trace.TraceError("PerformOnce: Result of operation cannot be null for '{0}'", resultKey);
                }
                else
                {
                    if (!client.Set(resultKey, result))
                        System.Diagnostics.Trace.TraceWarning("PerformOnce: Failed to save result for '{0}'", resultKey);
                    output = (T)result;
                }
            };

            if (!PerformOnce(client, maxOperationTimeout, lockName, op))
            {
                // So someone else is already performing operation or has just finished.
                // So try to read the new value until maximum time will pass.
                DateTime start = DateTime.Now;
                object res;
                bool success;
                do
                {
                    res = client.Get(resultKey, out success);
                    if (success && res != null)
                        break;
                    System.Threading.Thread.Sleep(250);
                } while (DateTime.Now.Subtract(start).TotalSeconds <= maxOperationTimeout);
                if (!success || res == null)
                    System.Diagnostics.Trace.TraceError("PerformOnce: Timed out to retrieve result for '{0}'", resultKey);
                else
                    output = (T)res;
            }
            return output;
        }

        /// <summary>
        /// Perform an operation but only if no other instance is already performing it.
        /// Thread safe.
        /// </summary>
        /// <param name="client">store</param>
        /// <param name="maxOperationTimeout">max lasting time of the operation; in seconds</param>
        /// <param name="lockName">process-wide unique name</param>
        /// <param name="operation">action to take</param>
        /// <returns>true if operation performed; false if any other instance is already performing or has just finished</returns>
        public static bool PerformOnce(ICache client, int maxOperationTimeout, string lockName, Action operation)
        {
            if (maxOperationTimeout < 1)
                throw new ArgumentOutOfRangeException("maxOperationTimeout");
            if (client == null)
                throw new ArgumentNullException("client");
            if (operation == null)
                throw new ArgumentNullException("operation");
            if (lockName == null)
                throw new ArgumentNullException("lockName");
            string key = "lock:" + lockName;
            if (client.Add(key, maxOperationTimeout, DateTime.Now.Ticks))
            {
                // if we could Add, it means there was not lock there yet, so go ahead
                try
                {
                    operation();
                    return true;
                }
                finally
                {
                    if (!client.Remove(key))
                        System.Diagnostics.Trace.TraceWarning("PerformOnce: Failed to remove lock item '{0}'", key);
                }
            }
            else
            {
                // key already there, someone was faster
                bool success;
                long ticks = client.Get<long>(key, out success);
                if (success && ticks > 0)
                {
                    // check the lock expiry time
                    if (new DateTime(ticks).AddSeconds(maxOperationTimeout) < DateTime.Now)
                    {
                        // other process has failed to release the lock?
                        if (!client.Remove(key))
                            System.Diagnostics.Trace.TraceWarning("PerformOnce: Failed to remove stale lock item '{0}'", key);
                        operation();
                        return true;
                    }
                }// else 0=not exists, operation has just finished
                return false;// operation in progress or has just finished - result may be available
            }
        }

        /// <summary>
        /// Perform the operation within a system-wide lock.
        /// </summary>
        /// <remarks>This is a general use method.</remarks>
        /// <param name="lockTimeout">in ms to wait for lock acquision</param>
        /// <param name="lockName">system-wide mutex name</param>
        /// <param name="operation">action to take within the lock</param>
        /// <returns>false if lock attempt failed/timed-out</returns>
        public static bool PerformCrossProcess(int lockTimeout, string lockName, Action operation)
        {
            if (lockTimeout < 1)
                throw new ArgumentOutOfRangeException("lockTimeout");
            if (operation == null)
                throw new ArgumentNullException("operation");
            if (lockName == null)
                throw new ArgumentNullException("lockName");
            bool locked;
            using (var sem = new System.Threading.Semaphore(1, 1, "Global\\SC_" + lockName, out locked))
            {
                if (locked)
                    locked = sem.WaitOne();// created, so use
                else
                    try
                    {
                        locked = sem.WaitOne(lockTimeout);
                    }
                    catch (System.Threading.AbandonedMutexException)
                    {
                        // other prosess has been killed while owing the mutex; assume we own it now and can proceed
                        locked = true;
                    }
                if (locked)
                    try
                    {
                        operation();
                    }
                    finally
                    {
                        sem.Release();
                    }
                /*
                using (var mutex = new System.Threading.Mutex(true, "Global\\MC_" + lockName, out locked))
                    try
                    {
                        if (!locked)
                            try
                            {
                                locked = mutex.WaitOne(lockTimeout, false);
                            }
                            catch (System.Threading.AbandonedMutexException)
                            {
                                // other prosess has been killed while owing the mutex; assume we own it now and can proceed
                                locked = true;
                            }
                        if (locked)
                            operation();
                    }
                    finally
                    {
                        if (locked)
                            mutex.ReleaseMutex();
                    }
                 */
            }
            return locked;
        }
    }
}